package	model.code;

import editor.Images;
import lang.Language;
import lang.asm.ASM;
import model.Project;
import model.caret.CaretRef;
import model.code.line.LineList;
import model.code.line.SingleLine;
import model.symbol.Scope;
import model.symbol.Symbol;
import observer.ObservedList;
import util.FileUtil;

import javax.swing.*;
import java.io.File;
import java.util.ArrayList;

/**
 * This class is for files containing code of some language.
 *
 * Although it may be conceptually different from a {@link Section}, a CodeFile is a Section in that
 * it is a container of {@link CodeFileChunk} objects.
 *
 * @author Neil Dickson
 */
public class CodeFile extends Section implements ProjectFile,Symbol {
	private Project							project;
	private	Directory						directory;

	private	ObservedList<Rowable>			authors;

	/**
	 * True indicates that this file is an include file
	 */
	private	boolean							isIncludeFile;
	private	final boolean					isExternal;

	private	Language						language;

	private final CaretRef					caretRef;

	public CodeFile(String name, String description, boolean includeFile) {
		this(name,description,ASM.language,null,false,includeFile);
	}
	public CodeFile(String name, String description, Language language, Directory directory, boolean isExternal, boolean includeFile) {
		super(name, description);
		authors = new ObservedList<Rowable>();
		isIncludeFile = includeFile;
		this.isExternal = isExternal;
		this.language = language;
		caretRef = new CaretRef();
		project = null;
		this.directory = directory;
		getName().addObserver(new SingleLine.LineObserver(){
			public void valueChanged(final SingleLine s, String oldValue) {
				// Check for invalid characters
				String adjustedName = FileUtil.removeBadFilenameChars(s.toString());
				// Don't update the file's name unless the name is valid.
				if (adjustedName==null) {
					String dir = getDirectoryPath();
					File file = new File(dir+oldValue);
					boolean success = false;
					if (file.isFile()) {
						success = file.renameTo(new File(dir+s.toString()));
					}
					// If the file couldn't be renamed (e.g. duplicate name), revert to its previous name.
					if (!success) {
						adjustedName = oldValue;
					}
				}
				if (adjustedName!=null) {
					final String finalAdjustedName = adjustedName;
					final boolean changed = !adjustedName.equals(oldValue);
					final SingleLine.LineObserver thisStringObserver = this;
					// If the name contained invalid characters, it must be updated after other listeners are
					// notified of the change, since in the case of from a DocumentEvent, an error will be
					// thrown if the Document is modified before return from the DocumentListener.
					// TODO: Come up with a better solution than this.
					new Thread() {
						public void run() {
							while (true) {
								try {
									if (changed) {
										s.replaceAll(finalAdjustedName);
									}
									else {
										s.replaceAll(thisStringObserver,finalAdjustedName);
									}
									break;
								}
								catch (IllegalStateException e) {
									// Wait until the Document is free
									try {
										Thread.sleep(0);
									}
									catch (InterruptedException e1) {
										// This exception doesn't matter at all
									}
								}
							}
						}
					}.start();
				}
			}
		});
	}

	public CodeFile(String name) {
		this(name,"",false);
	}

	public CodeFile(String name, Language language, Directory directory, boolean isExternal,boolean isIncludeFile) {
		this(name,"",language,directory,isExternal,isIncludeFile);
	}

	public String toString() {
		return name.toString();
	}

	public ObservedList<Rowable> getAuthors() {
		return authors;
	}

	/**
	 * @return true if this is the file containing the main entry point function,
	 *         or if there is no main entry point function and this is the first file in the file list
	 */
	public boolean isMainFile() {
		Function function = project.getEntryPoint();
		if (function!=null && function.getFile()==this) {
			return true;
		}
		if (function==null) {
			ArrayList<CodeFile> files = project.getDirectory().getAllFiles();
			return (!files.isEmpty() && files.get(0)==this);
		}
		return false;
	}

	public boolean isIncludeFile() {
		return isIncludeFile;
	}

	public boolean isExternal() {
		return isExternal;
	}

	public void setAuthors(String[] authors) {
		this.authors = new ObservedList<Rowable>(authors.length);
		for (String string : authors) {
			this.authors.add(Rowable.AUTHORS_DUMMY.copy(this,this.authors.size(),new String[]{string}));
		}
	}

	public ImageIcon getIcon() {
		if (name.toString().endsWith(".asm")) {
			return Images.ASM_FILE_ICON;
		}
		else if (name.toString().endsWith(".inc")) {
			return Images.ASM_INCLUDE_FILE_ICON;
		}
		return Images.MISC_CODE_FILE_ICON;
	}

	public Scope getGlobalScope() {
		if (project!=null) {
			return project.getScope();
		}
		return null;
	}

	public Project getProject() {
		return project;
	}
	public void setProject(Project project) {
		this.project = project;
	}

	public CaretRef getCaretRef() {
		return caretRef;
	}

	public String getDirectoryPath() {
		return directory.getPath();
	}
	public String getFilePath() {
		return getDirectoryPath()+getName().toString();
	}

	public Directory getDirectory() {
		return directory;
	}

	public void setDirectory(Directory directory) {
		this.directory = directory;
	}

	public Language getLanguage() {
		return language;
	}

	public CodeFile copy() {
		CodeFile that = new CodeFile(getName().toString(),getDescription().toString(),isIncludeFile());
		that.project = project;
		that.directory = directory;
		that.authors = new ObservedList<Rowable>(authors.size());
		for (Rowable author : authors) {
			that.authors.add(author.copy(that,that.authors.size()));
		}
		that.getSeeAlso().ensureCapacity(getSeeAlso().size());
		for (Rowable sa : getSeeAlso()) {
			that.getSeeAlso().add(sa.copy(that,that.getSeeAlso().size()));
		}
		// addAll() is used instead of = so that the parents of the elements are set appropriately
		// and that remains an observer of its content
		that.getContent().addAll(getContent().copy());
		return that;
	}

	public LineList getLineList(int locType, int locIndex) {
		int row = locIndex>>>16;
		int col = locIndex&0xFFFF;
		ObservedList<? extends Rowable> table = null;
		if (locType==LOCATION_AUTHORS) {
			table = authors;
		}
		else {
			return super.getLineList(locType, locIndex);
		}
		if (table!=null && table.size()>row && table.get(row).getStrings().length>col) {
			return table.get(row).getStrings()[col];
		}
		return null;
	}

	public LineList getNextLineList(LineList list) {
		int locType = list.getLocationType();
		int locIndex = list.getLocationIndex();
		if (locType==LOCATION_NAME) {
			return description;
		}
		if (locType==LOCATION_DESCRIPTION) {
			if (getSeeAlso().size()>0) {
				return getLineList(LOCATION_SEE_ALSO,0);
			}
			if (authors.size()>0) {
				return getLineList(LOCATION_AUTHORS,0);
			}
			return null;
		}
		ObservedList<? extends Rowable> table = null;
		int row = locIndex>>>16;
		int col = locIndex&0xFFFF;
		if (locType==LOCATION_SEE_ALSO) {
			table = getSeeAlso();
		}
		if (locType==LOCATION_AUTHORS) {
			table = authors;
		}
		if (table!=null) {
			if (col+1<table.get(row).getSize()) {
				return getLineList(locType,locIndex+1);
			}
			if (row+1<table.size()) {
				return getLineList(locType,(row+1)<<16);
			}
			if (locType==LOCATION_SEE_ALSO && authors.size()>0) {
				return getLineList(LOCATION_AUTHORS,0);
			}
		}
		return null;
	}

	public LineList getPrevLineList(LineList list) {
		int locType = list.getLocationType();
		int locIndex = list.getLocationIndex();
		if (locType==LOCATION_NAME) {
			return null;
		}
		if (locType==LOCATION_DESCRIPTION) {
			return name;
		}
		ObservedList<? extends Rowable> table = null;
		int row = locIndex>>>16;
		int col = locIndex&0xFFFF;
		if (locType==LOCATION_SEE_ALSO) {
			table = getSeeAlso();
		}
		if (locType==LOCATION_AUTHORS) {
			table = authors;
		}
		if (table!=null) {
			if (col-1>=0) {
				return getLineList(locType,locIndex-1);
			}
			if (row-1>=0) {
				return getLineList(locType,(row-1)<<16);
			}
			if (locType==LOCATION_SEE_ALSO) {
				return description;
			}
			return (getSeeAlso().size()>0) ? getSeeAlso().getLast().getStrings()[getSeeAlso().getLast().getSize()-1] : description;
		}
		return null;
	}

	public LineList getLastLineList() {
		if (authors.size()>0) {
			return authors.getLast().getStrings()[authors.getLast().getSize()-1];
		}
		if (getSeeAlso().size()>0) {
			return getSeeAlso().getLast().getStrings()[getSeeAlso().getLast().getSize()-1];
		}
		return description;
	}

	public LineList getLastLineListPlus() {
		CodeFileChunk last = getLast();
		if (last instanceof Section) {
			last = ((Section)last).getLast();
		}
		return last.getLastLineList();
	}
}
